package com.donkey.http;

import org.apache.http.Consts;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.NameValuePair;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.MessageConstraints;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * <h3>Http 常用方法封装</h3>
 * <p>
 * Cookie 解析办法: <code>List<HttpCookie> cookies = HttpCookie.parse(header.getValue());</code>
 * </p>
 * <p>
 * HttpClient 4.3 连接池参数配置及源码解读: http://blog.csdn.net/umke888/article/details/54881946
 * </p>
 */
public class Http {
    private static final String DEFAULT_CHARSET_NAME = "UTF-8";
    private static final Charset DEFAULT_CHARSET = Charset.forName(DEFAULT_CHARSET_NAME);
    private int maxTotal = 1000; //最大链接数
    private int maxPerRoute = 50; //设置每个主机地址的并发数
    private int connectTimeout = 3000; //创建连接的最长时间
    private int socketTimeout = 5000; //数据传输的最长时间
    private int connectionRequestTimeout = 1000; //从连接池中获取到连接的最长时间
    private boolean staleConnectionCheckEnabled = true; //提交请求前测试连接是否可用
    private PoolingHttpClientConnectionManager httpClientConnectionManager;
    private RequestConfig requestConfig = null;
    private CloseableHttpClient httpClient = null;
    private HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() { //请求重试处理

        public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
            if (executionCount >= 2) { //如果已经重试了2次，就放弃
                return false;
            }
            if (exception instanceof NoHttpResponseException) { //如果服务器丢掉了连接，那么就重试
                return true;
            }
            if (exception instanceof SSLHandshakeException) { //不要重试SSL握手异常
                return false;
            }
            if (exception instanceof InterruptedIOException) { //超时
                return false;
            }
            if (exception instanceof UnknownHostException) { //目标服务器不可达
                return false;
            }
            if (exception instanceof ConnectTimeoutException) { //连接被拒绝
                return false;
            }
            if (exception instanceof SSLException) { //ssl握手异常
                return false;
            }
            HttpClientContext clientContext = HttpClientContext.adapt(context);
            HttpRequest request = clientContext.getRequest();
            // 如果请求是幂等的，就再次尝试
            if (!(request instanceof HttpEntityEnclosingRequest)) {
                return true;
            }
            return false;
        }
    };
    private HttpExpiredConnectionEvictor httpExpiredConnectionEvictor;

    public HttpClientConnectionManager getHttpClientConnectionManager() {
        if (httpClientConnectionManager == null) {
            synchronized (this) {
                if (httpClientConnectionManager == null) {
                    httpClientConnectionManager = new PoolingHttpClientConnectionManager();
                    httpClientConnectionManager.setMaxTotal(maxTotal);
                    httpClientConnectionManager.setDefaultMaxPerRoute(maxPerRoute);

                    // socket 配置(默认配置)
                    SocketConfig socketConfig = SocketConfig.custom()
                            .setTcpNoDelay(true)     //是否立即发送数据，设置为 true 会关闭 Socket 缓冲，默认为 false
                            .setSoReuseAddress(true) //是否可以在一个进程关闭 Socket 后，即使它还没有释放端口，其它进程还可以立即重用端口
                            .setSoTimeout(socketTimeout) //接收数据的等待超时时间，单位ms
                            .setSoLinger(60)         //关闭 Socket 时，要么发送完所有数据，要么等待60s后，就关闭连接，此时socket.close()是阻塞的
                            .setSoKeepAlive(true)    //开启监视TCP连接是否有效
                            .build();
                    httpClientConnectionManager.setDefaultSocketConfig(socketConfig);

                    // HTTP connection 相关配置(默认配置)
                    // 消息约束
                    MessageConstraints messageConstraints = MessageConstraints.custom()
                            .setMaxHeaderCount(200)
                            .setMaxLineLength(2000)
                            .build();
                    // Http connection 相关配置
                    ConnectionConfig connectionConfig = ConnectionConfig.custom()
                            .setMalformedInputAction(CodingErrorAction.IGNORE)
                            .setUnmappableInputAction(CodingErrorAction.IGNORE)
                            .setCharset(Consts.UTF_8)
                            .setMessageConstraints(messageConstraints)
                            .build();
                    // 一般不修改 HTTP connection 相关配置，故不设置
                    //httpClientConnectionManager.setDefaultConnectionConfig(connectionConfig);

                    // 清理无效的连接
                    httpExpiredConnectionEvictor = new HttpExpiredConnectionEvictor(httpClientConnectionManager);
                }
            }
        }
        return httpClientConnectionManager;
    }

    protected RequestConfig getRequestConfig() {
        if (requestConfig == null) {
            synchronized (this) {
                if (requestConfig == null) {
                    requestConfig = RequestConfig.custom()
                            .setConnectTimeout(connectTimeout)
                            .setSocketTimeout(socketTimeout)
                            .setConnectionRequestTimeout(connectionRequestTimeout)
                            .setStaleConnectionCheckEnabled(staleConnectionCheckEnabled) //检查是否为陈旧的连接，默认为true，类似testOnBorrow
                            .build();
                }
            }
        }
        return requestConfig;
    }

    protected CloseableHttpClient getHttpClient() {
        if (httpClient == null) {
            synchronized (this) {
                if (httpClient == null) {
                    httpClient = HttpClients.custom()
                            .setConnectionManager(getHttpClientConnectionManager())
                            .setDefaultRequestConfig(getRequestConfig())
                            .setRetryHandler(httpRequestRetryHandler)
                            .build();
                }
            }
        }
        return httpClient;
    }

    protected void closeQuietly(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
            }
        }
    }

    public void shutdown() {
        if (httpExpiredConnectionEvictor != null) {
            httpExpiredConnectionEvictor.shutdown();
        }
        if (httpClientConnectionManager != null) {
            httpClientConnectionManager.shutdown();
        }
    }

    public String get(String url,
                      Map<String, String> requestHeaders,
                      Map<String, String> parameters) throws IOException {
        return get(url, requestHeaders, parameters, DEFAULT_CHARSET, null, DEFAULT_CHARSET);
    }

    public String get(String url,
                      Map<String, String> requestHeaders,
                      Map<String, String> parameters,
                      Charset inputCharset,
                      Map<String, String> responseHeaders,
                      Charset outputCharset) throws IOException {
        String queryString = null;
        if (parameters != null) {
            List<NameValuePair> pairs = new ArrayList<>();
            for (String key : parameters.keySet()) {
                pairs.add(new BasicNameValuePair(key, parameters.get(key)));
            }
            queryString = URLEncodedUtils.format(pairs, inputCharset);
        }
        return get(url, requestHeaders, queryString, null, outputCharset);
    }

    public String get(String url) throws IOException {
        return get(url, null, null, null, DEFAULT_CHARSET);
    }

    public String get(String url,
                      Map<String, String> requestHeaders,
                      String queryString) throws IOException {
        return get(url, requestHeaders, queryString, null, DEFAULT_CHARSET);
    }

    public String get(String url,
                      Map<String, String> requestHeaders,
                      String queryString,
                      Map<String, String> responseHeaders,
                      Charset outputCharset) throws IOException {
        if (queryString != null) {
            url = url + (url.indexOf('?') == -1 ? '?' : '&') + queryString;
        }
        HttpGet httpGet = new HttpGet(url);
        httpGet.setConfig(getRequestConfig());
        return execute(httpGet, requestHeaders, responseHeaders, outputCharset);
    }

    public void get(String url,
                    Map<String, String> requestHeaders,
                    Map<String, String> parameters,
                    Charset inputCharset,
                    Map<String, String> responseHeaders,
                    OutputStream outputStream) throws IOException {
        String queryString = null;
        if (parameters != null) {
            List<NameValuePair> pairs = new ArrayList<>();
            for (String key : parameters.keySet()) {
                pairs.add(new BasicNameValuePair(key, parameters.get(key)));
            }
            queryString = URLEncodedUtils.format(pairs, inputCharset);
        }
        get(url, requestHeaders, queryString, responseHeaders, outputStream);
    }

    public void get(String url,
                    Map<String, String> requestHeaders,
                    String queryString,
                    Map<String, String> responseHeaders,
                    OutputStream outputStream) throws IOException {
        if (queryString != null) {
            url = url + (url.indexOf('?') == -1 ? '?' : '&') + queryString;
        }
        HttpGet httpGet = new HttpGet(url);
        httpGet.setConfig(getRequestConfig());
        execute(httpGet, requestHeaders, responseHeaders, outputStream);
    }

    public String post(String url,
                       Map<String, String> requestHeaders,
                       String content) throws IOException {
        return post(url, requestHeaders, content, DEFAULT_CHARSET, null, DEFAULT_CHARSET);
    }

    public String post(String url,
                       Map<String, String> requestHeaders,
                       String content,
                       Charset inputCharset,
                       Map<String, String> responseHeaders,
                       Charset outputCharset) throws IOException {
        StringEntity entity = null;
        if (content != null) {
            entity = new StringEntity(content, inputCharset);
        }
        return postEntity(url, requestHeaders, entity, responseHeaders, outputCharset);
    }

    public String post(String url,
                       Map<String, String> requestHeaders,
                       Map<String, String> parameters) throws IOException {
        return post(url, requestHeaders, parameters, DEFAULT_CHARSET, null, DEFAULT_CHARSET);
    }

    public String post(String url,
                       Map<String, String> requestHeaders,
                       Map<String, String> parameters,
                       Charset inputCharset,
                       Map<String, String> responseHeaders,
                       Charset outputCharset) throws IOException {
        UrlEncodedFormEntity entity = null;
        if (parameters != null) {
            List<NameValuePair> pairs = new ArrayList<>();
            for (String key : parameters.keySet()) {
                pairs.add(new BasicNameValuePair(key, parameters.get(key)));
            }
            entity = new UrlEncodedFormEntity(pairs, inputCharset);
        }
        return postEntity(url, requestHeaders, entity, responseHeaders, outputCharset);
    }

    public void post(String url,
                     Map<String, String> requestHeaders,
                     String content,
                     Charset charset,
                     Map<String, String> responseHeaders,
                     OutputStream outputStream) throws IOException {
        StringEntity entity = null;
        if (content != null) {
            entity = new StringEntity(content, charset);
        }
        postEntity(url, requestHeaders, entity, responseHeaders, outputStream);
    }

    public void post(String url,
                     Map<String, String> requestHeaders,
                     Map<String, String> parameters,
                     Charset charset,
                     Map<String, String> responseHeaders,
                     OutputStream outputStream) throws IOException {
        UrlEncodedFormEntity entity = null;
        if (parameters != null) {
            List<NameValuePair> pairs = new ArrayList<>();
            for (String key : parameters.keySet()) {
                pairs.add(new BasicNameValuePair(key, parameters.get(key)));
            }
            entity = new UrlEncodedFormEntity(pairs, charset);
        }
        postEntity(url, requestHeaders, entity, responseHeaders, outputStream);
    }

    public String upload(String url,
                         Map<String, String> requestHeaders,
                         Map<String, String> parameters,
                         Map<String, File> files) throws IOException {
        return upload(url, requestHeaders, parameters, files, DEFAULT_CHARSET, null, DEFAULT_CHARSET);
    }

    public String upload(String url,
                         Map<String, String> requestHeaders,
                         Map<String, String> parameters,
                         Map<String, File> files,
                         Charset inputCharset,
                         Map<String, String> responseHeaders,
                         Charset outputCharset) throws IOException {
        MultipartEntityBuilder entity = MultipartEntityBuilder.create();
        ContentType contentType = ContentType.create("text/plain", inputCharset);
        if (parameters != null) {
            for (String key : parameters.keySet()) {
                entity.addTextBody(key, parameters.get(key), contentType);
            }
        }
        if (files != null) {
            for (String key : files.keySet()) {
                entity.addBinaryBody(key, files.get(key));
            }
        }
        return postEntity(url, requestHeaders, entity.build(), responseHeaders, outputCharset);
    }

    private String postEntity(String url,
                              Map<String, String> requestHeaders,
                              HttpEntity entity,
                              Map<String, String> responseHeaders,
                              Charset outputCharset) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        httpPost.setConfig(getRequestConfig());
        if (entity != null) {
            httpPost.setEntity(entity);
        }
        return execute(httpPost, requestHeaders, responseHeaders, outputCharset);
    }

    private void postEntity(String url,
                            Map<String, String> requestHeaders,
                            HttpEntity entity,
                            Map<String, String> responseHeaders,
                            OutputStream outputStream) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        httpPost.setConfig(getRequestConfig());
        if (entity != null) {
            httpPost.setEntity(entity);
        }
        execute(httpPost, requestHeaders, responseHeaders, outputStream);
    }

    /**
     * HTTP 请求
     *
     * @param request         请求数据
     * @param requestHeaders  请求头信息
     * @param responseHeaders 用于接收响应头信息
     * @param outputCharset   响应数据编码
     * @throws IOException 异常
     */
    private String execute(HttpUriRequest request,
                           Map<String, String> requestHeaders,
                           Map<String, String> responseHeaders,
                           Charset outputCharset) throws IOException {
        CloseableHttpResponse response = null;
        try {
            if (requestHeaders != null) {
                for (String key : requestHeaders.keySet()) {
                    request.addHeader(key, requestHeaders.get(key));
                }
            }
            response = getHttpClient().execute(request, HttpClientContext.create());
            int status = response.getStatusLine().getStatusCode();
            if (status != 200) {
                throw new HttpResponseException(request.getURI().toString(), request.getMethod(), status, response.getStatusLine().getReasonPhrase());
            }
            if (responseHeaders != null) {
                for (Header header : response.getAllHeaders()) {
                    responseHeaders.put(header.getName(), header.getValue());
                }
            }
            return EntityUtils.toString(response.getEntity(), outputCharset);
        } finally {
            closeQuietly(response);
        }
    }

    /**
     * HTTP 请求
     *
     * @param request         请求数据
     * @param requestHeaders  请求头信息
     * @param responseHeaders 用于接收响应头信息
     * @param outputStream    用于接收输出内容
     * @throws IOException 异常
     */
    private void execute(HttpUriRequest request,
                         Map<String, String> requestHeaders,
                         Map<String, String> responseHeaders,
                         OutputStream outputStream) throws IOException {
        CloseableHttpResponse response = null;
        InputStream in = null;
        try {
            if (requestHeaders != null) {
                for (String key : requestHeaders.keySet()) {
                    request.addHeader(key, requestHeaders.get(key));
                }
            }
            response = getHttpClient().execute(request, HttpClientContext.create());
            int status = response.getStatusLine().getStatusCode();
            if (status != 200) {
                throw new HttpResponseException(request.getURI().toString(), request.getMethod(), status, response.getStatusLine().getReasonPhrase());
            }
            if (responseHeaders != null) {
                for (Header header : response.getAllHeaders()) {
                    responseHeaders.put(header.getName(), header.getValue());
                }
            }
            in = response.getEntity().getContent();
            byte[] buffer = new byte[1024];
            for (int length; (length = in.read(buffer)) > -1; ) {
                outputStream.write(buffer, 0, length);
            }
            outputStream.flush();
        } finally {
            // in.close() 作用就是将用完的连接释放，下次请求可以复用，这里特别注意的是，
            // 如果不使用 in.close(); 而仅仅使用 response.close(); 结果就是连接会被关闭，并且不能被复用，这样就失去了采用连接池的意义。
            closeQuietly(outputStream);
            closeQuietly(in);
            closeQuietly(response);
        }
    }

    public void setMaxTotal(int maxTotal) {
        this.maxTotal = maxTotal;
    }

    public void setMaxPerRoute(int maxPerRoute) {
        this.maxPerRoute = maxPerRoute;
    }

    public void setConnectTimeout(int connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public void setSocketTimeout(int socketTimeout) {
        this.socketTimeout = socketTimeout;
    }

    public void setConnectionRequestTimeout(int connectionRequestTimeout) {
        this.connectionRequestTimeout = connectionRequestTimeout;
    }

    public void setStaleConnectionCheckEnabled(boolean staleConnectionCheckEnabled) {
        this.staleConnectionCheckEnabled = staleConnectionCheckEnabled;
    }

    public void setHttpClientConnectionManager(PoolingHttpClientConnectionManager httpClientConnectionManager) {
        this.httpClientConnectionManager = httpClientConnectionManager;
    }

    public void setRequestConfig(RequestConfig requestConfig) {
        this.requestConfig = requestConfig;
    }
}
